package org.bindinghelper;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Modifier;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import net.sf.cglib.proxy.Enhancer;

import org.jdesktop.observablecollections.ObservableList;
import org.jdesktop.observablecollections.ObservableListListener;


public class ObservableBeanFactory
{
	private static final int INFINITE = -9999;

	@SuppressWarnings("unchecked")
	public static <T, V extends T> V observableBean(V obj, Class<T> clazz) throws Exception
	{
		V ret = null;
		if(obj == null)
		{
			ret = null;
		}
		else if (Modifier.isFinal(clazz.getModifiers())
				|| (Enhancer.isEnhanced(obj.getClass())
						&& obj instanceof IObservable))
		{
			ret = obj;
		}
		else
		{
			ObservableMethodInterceptor interceptor = new ObservableMethodInterceptor();
            ret = (V)Enhancer.create(obj.getClass(), new Class[]{IWriteReplace.class, IObservable.class, Serializable.class}, interceptor);
            BeanUtil.copyBeanData(CopyUtil.getCopy(obj), ret);
            interceptor.initPCS(ret, obj.getClass());
		}
		return ret;
	}

	public static <V, A extends V, T, B extends T> V deepObservableBean(A obj, Class<V> clazz) throws Exception
	{
		return deepObservableBean(obj, clazz, INFINITE);
	}

	@SuppressWarnings("unchecked")
	public static <V, A extends V> V deepObservableBean(A obj, Class<V> clazz, int levels) throws Exception
	{
	    V ret = observableBean(obj, clazz);
		if((levels > 0 || levels <= INFINITE)
				&& obj != null)
		{
			for(PropertyDescriptor pd : Introspector.getBeanInfo( clazz ).getPropertyDescriptors())
			{
				if(pd.getWriteMethod() != null
						&& pd.getReadMethod() != null)
				{
					if(ObservableList.class.isAssignableFrom(pd.getPropertyType())
							|| pd.getPropertyType().equals(List.class))
					{
						Object listObject = pd.getReadMethod().invoke(obj, new Object[]{});
						if(listObject != null)
						{
							List list = (List)listObject;
							List newList = observableSerializableList();
							newList.addAll(list);
							pd.getWriteMethod().invoke(ret, new Object[]{((Class)pd.getPropertyType()).cast(newList)});
							for(int i = 0; i < list.size(); i++)
							{
								Object listElement = list.get(i);
								if(listElement != null)
								{
									newList.set(i, deepObservableBean(listElement, (Class)listElement.getClass(), levels - 1));
								}
							}
						}
					}
					else if(Collection.class.isAssignableFrom(pd.getPropertyType()))
					{
						//TODO Add an observable Set data type
						Object collectionObject = pd.getReadMethod().invoke(obj, new Object[]{});
						pd.getWriteMethod().invoke(ret, new Object[]{collectionObject});
					}
					else if(pd.getPropertyType().equals(Object.class))
					{
						Object propertyObject = pd.getReadMethod().invoke(obj, new Object[]{});
						if(propertyObject != null)
						{
                            try
                            {
                                pd.getWriteMethod().invoke(ret, new Object[]{deepObservableBean(propertyObject, (Class)propertyObject.getClass(), levels - 1)});
                            }
                            catch(Throwable t)
                            {
                                // Ignore
                            }
						}
					}
					else if(!pd.getPropertyType().isPrimitive())
					{
						Object propertyObject = pd.getReadMethod().invoke(obj, new Object[]{});
						if(propertyObject != null)
						{
                            try
                            {
                                pd.getWriteMethod().invoke(ret, new Object[]{deepObservableBean(propertyObject, (Class)pd.getPropertyType(), levels - 1)});
                            }
                            catch(Throwable t)
                            {
                                // Ignore
                            }
						}
					}
				}
			}
		}
		return ret;
	}

	@SuppressWarnings("unused")
    private static <T> List<T> observableSerializableList(List<T> originalList)
	{
	    List<T> ret = null;
	    if(originalList == null)
	    {
	        ret = null;
	    }
	    else if(Enhancer.isEnhanced(originalList.getClass())
	            && originalList instanceof IWriteReplace)
	    {
	        ret = originalList;
	    }
	    else
	    {
	        ret = observableSerializableList();
	        ret.addAll(originalList);
	    }
	    return ret;
	}

    private static <T> List<T> observableSerializableList()
	{
	    return new ObservableListImpl<T>(new ArrayList<T>(), true);
	}

	//Copy of ObservableListImpl from BeansBinding but, this one is serializable
	// and serializes to an arraylist.
	private static final class ObservableListImpl<E> extends AbstractList<E>
	implements ObservableList<E>, Serializable {
        private static final long serialVersionUID = 1599898635073844643L;

        private final boolean supportsElementPropertyChanged;
	    private List<E> list;
	    private List<ObservableListListener> listeners;

	    ObservableListImpl(List<E> list, boolean supportsElementPropertyChanged) {
	        this.list = list;
	        listeners = new CopyOnWriteArrayList<ObservableListListener>();
	        this.supportsElementPropertyChanged = supportsElementPropertyChanged;
	    }

	    @Override
        public E get(int index) {
	        return list.get(index);
	    }

	    @Override
        public int size() {
	        return list.size();
	    }

	    @Override
        public E set(int index, E element) {
	        E oldValue = list.set(index, element);
	        for (ObservableListListener listener : listeners) {
	            listener.listElementReplaced(this, index, oldValue);
	        }
	        return oldValue;
	    }

	    @Override
        public void add(int index, E element) {
	        list.add(index, element);
	        modCount++;
	        for (ObservableListListener listener : listeners) {
	            listener.listElementsAdded(this, index, 1);
	        }
	    }

	    @Override
        public E remove(int index) {
	        E oldValue = list.remove(index);
	        modCount++;
	        for (ObservableListListener listener : listeners) {
	            listener.listElementsRemoved(this, index,
	                                         java.util.Collections.singletonList(oldValue));
	        }
	        return oldValue;
	    }

	    @Override
        public boolean addAll(Collection<? extends E> c) {
	        return addAll(size(), c);
	    }

	    @Override
        public boolean addAll(int index, Collection<? extends E> c) {
	        if (list.addAll(index, c)) {
	            modCount++;
	            for (ObservableListListener listener : listeners) {
	                listener.listElementsAdded(this, index, c.size());
	            }
	        }
	        return false;
	    }

	    @Override
        public void clear() {
	        List<E> dup = new ArrayList<E>(list);
	        list.clear();
	        modCount++;
	        if (dup.size() != 0) {
	            for (ObservableListListener listener : listeners) {
	                listener.listElementsRemoved(this, 0, dup);
	            }
	        }
	    }

	    @Override
        public boolean containsAll(Collection<?> c) {
	        return list.containsAll(c);
	    }

	    @Override
        public <T> T[] toArray(T[] a) {
	        return list.toArray(a);
	    }

	    @Override
        public Object[] toArray() {
	        return list.toArray();
	    }

	    @SuppressWarnings("unused")
        private void fireElementChanged(int index) {
	        for (ObservableListListener listener : listeners) {
	            listener.listElementPropertyChanged(this, index);
	        }
	    }

	    public void addObservableListListener(ObservableListListener listener) {
	        listeners.add(listener);
	    }

	    public void removeObservableListListener(ObservableListListener listener) {
	        listeners.remove(listener);
	    }

	    public boolean supportsElementPropertyChanged() {
	        return supportsElementPropertyChanged;
	    }

	    public Object writeReplace()
	    {
	        List<E> ret = new ArrayList<E>();
	        ret.addAll(this);
	        return ret;
	    }
	}
}
